﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;

namespace RazorEngineCore
{
	public class RazorEngine : IRazorEngine
	{
		public IRazorEngineCompiledTemplate<T> Compile<T>(string content, Action<IRazorEngineCompilationOptionsBuilder> builderAction = null) where T : IRazorEngineTemplate
		{
			IRazorEngineCompilationOptionsBuilder compilationOptionsBuilder = new RazorEngineCompilationOptionsBuilder();

			compilationOptionsBuilder.AddAssemblyReference(typeof(T).Assembly);
			compilationOptionsBuilder.Inherits(typeof(T));

			builderAction?.Invoke(compilationOptionsBuilder);

			MemoryStream memoryStream = this.CreateAndCompileToStream(content, compilationOptionsBuilder.Options);

			return new RazorEngineCompiledTemplate<T>(memoryStream);
		}

		public Task<IRazorEngineCompiledTemplate<T>> CompileAsync<T>(string content, Action<IRazorEngineCompilationOptionsBuilder> builderAction = null) where T : IRazorEngineTemplate
		{
			return Task.Run(() => Compile<T>(content: content, builderAction: builderAction));
		}

		public IRazorEngineCompiledTemplate Compile(string content, Action<IRazorEngineCompilationOptionsBuilder> builderAction = null)
		{
			IRazorEngineCompilationOptionsBuilder compilationOptionsBuilder = new RazorEngineCompilationOptionsBuilder();
			compilationOptionsBuilder.Inherits(typeof(RazorEngineTemplateBase));

			builderAction?.Invoke(compilationOptionsBuilder);
			MemoryStream memoryStream = this.CreateAndCompileToStream(content, compilationOptionsBuilder.Options);

			return new RazorEngineCompiledTemplate(memoryStream);
		}

		public Task<IRazorEngineCompiledTemplate> CompileAsync(string content, Action<IRazorEngineCompilationOptionsBuilder> builderAction = null)
		{
			return Task.Run(() => this.Compile(content: content, builderAction: builderAction));
		}

		private MemoryStream CreateAndCompileToStream(string templateSource, RazorEngineCompilationOptions options)
		{
			templateSource = this.WriteDirectives(templateSource, options);

			RazorProjectEngine engine = RazorProjectEngine.Create(
				RazorConfiguration.Create(RazorLanguageVersion.Version_3_0, "unnamed", Array.Empty<RazorExtension>()),
				RazorProjectFileSystem.Create(@"."),
				(builder) => { builder.SetNamespace(options.TemplateNamespace); });

			string fileName = Path.GetRandomFileName();

			RazorSourceDocument document = RazorSourceDocument.Create(templateSource, fileName);

			RazorCodeDocument codeDocument = engine.Process(
				document,
				null,
				new List<RazorSourceDocument>(),
				new List<TagHelperDescriptor>());

			RazorCSharpDocument razorCSharpDocument = codeDocument.GetCSharpDocument();

			SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(razorCSharpDocument.GeneratedCode);

			CSharpCompilation compilation = CSharpCompilation.Create(
				fileName,
				new[]
				{
					syntaxTree
				},
				options.ReferencedAssemblies
				   .Select(ass => { return MetadataReference.CreateFromFile(ass.Location); })
					.Concat(options.MetadataReferences)
					.ToList(),
				new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

			MemoryStream memoryStream = new MemoryStream();

			EmitResult emitResult = compilation.Emit(memoryStream);

			if (!emitResult.Success)
			{
				RazorEngineCompilationException exception = new RazorEngineCompilationException()
				{
					Errors = emitResult.Diagnostics.ToList(),
					GeneratedCode = razorCSharpDocument.GeneratedCode
				};

				throw exception;
			}

			memoryStream.Position = 0;

			return memoryStream;
		}

		private string WriteDirectives(string content, RazorEngineCompilationOptions options)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine($"@inherits {options.Inherits}");

			foreach (string entry in options.DefaultUsings)
			{
				stringBuilder.AppendLine($"@using {entry}");
			}

			stringBuilder.Append(content);

			return stringBuilder.ToString();
		}
	}
}